Die Idee von aktionsbasierten Web-Frameworks ist nicht neu. Schon seit vielen Jahren scheinen besonders die beliebtesten serverbasierten Frameworks diesem Modell zu folgen. Spring MVC, Grails, Struts, Rails und Express basieren auf der einfachen, aber effektiven Idee, den Request/Response-Lebenszyklus ins Zentrum zu rücken und auf unnötige Abstraktionen zu verzichten.
Umso mehr verwunderte es, dass Java EE in diesem Bereich bisher nichts vorzuweisen hatte. Mit JavaServer Faces (JSF) bietet Java EE zwar ein mächtiges und flexibles Web-Framework, mit dem besonders formularbasierte Anwendungen sehr effektiv entwickelt werden können, jedoch ist JSF ein klassischer Vertreter der komponentenorientierten Frameworks, wie Wicket, GWT und Vaadin. An JSF ist sicherlich nichts falsch. Sowohl aktionsbasierte als auch komponentenorientierte Frameworks haben ihre Existenzberechtigung, abhängig von den individuellen Anforderungen des Projekts. Frameworks wie JSF versuchen, möglichst viele technische Details vor dem Entwickler zu verbergen und erlauben ihm, mit Hilfe vorgefertigter Komponenten viele Standardfälle sehr einfach umzusetzen. Obwohl man mit solchen Komponenten schnell zu vorzeigbaren Ergebnissen kommt, schwächeln sie oft bei der Möglichkeit, in Details einzugreifen und speziellere Anwendungsfälle umzusetzen. Im Gegensatz dazu verzichten aktionsbasierte Frameworks auf Abstraktionsschichten und rücken den http-Request/Response-Lebenszyklus in den Vordergrund.
JSR371
Die Geschichte der MVC-1.0-Spezifikation beginnt mit dem Java EE Community Survey, den Oracle Ende 2015 veröffentlichte, um Feedback für die Roadmap von Java EE 8 einzuholen. Dort fand sich auch die Frage, ob sich die Community zusätzlich zu JSF ein aktionsbasiertes Web-Framework wünschte. Über 60 Prozent der knapp 4 500 Teilnehmer sprachen sich für ein solches leichtgewichtigeres Web-Framework für die nächste Java-EE-Version aus.
Kurz darauf wurde mit JSR 371 ein entsprechender Java Specification Request ins Leben gerufen. Die Expert Group nahm schnell die Arbeit auf und stand direkt zu Beginn vor einer zentralen und wegweisenden Entscheidung. Sollte MVC 1.0 als Bestandteil von Java EE auf anderen Technologien der Plattform, wie JAX-RS und CDI, aufsetzen oder stattdessen ein von Java EE unabhängiges und dadurch potenziell leichtgewichtigeres API definieren? Nach langen, intensiven Diskussionen einigte man sich auf die enge Integration in Java EE. Warum auch sollte MVC 1.0 das Rad neu erfinden, wenn Aspekte wie das Routing, das Binden von Parametern und ein mächtiges Komponentenmodell schon von JAX-RS und CDI bereitgestellt werden?
Die große Enttäuschung kam dann zur JavaOne 2016. Nachdem Oracle die Entwicklung von Java EE 8 fast ein Jahr komplett hat schleifen lassen, wurde auf der Konferenz die aktualisierte Roadmap für Java EE 8 veröffentlicht. Der Plan enthielt einige neue Funktionen, die aus Oracles Sicht wesentlich dafür waren, Java EE für die Cloud fit zu machen. Allerdings wurden auch einige Themen von der Roadmap gestrichen, so auch leider die MVC-1.0-Spezifikation. Die Begründung, dass Microservices meist keine Benutzeroberfläche mitbringen und MVC somit nicht mehr benötigt würde, war zwar wenig überzeugend, aber die Entscheidung war zu diesem Zeitpunkt bereits gefallen. Besonders enttäuschend ist im Nachhinein, dass Oracle kein einziges der angekündigten neuen Features in Java EE 8 umgesetzt, sondern lediglich den Gesamtumfang reduziert hat.
Trotzdem war das Interesse der Java-EE-Community an MVC 1.0 auch weiterhin sehr groß. Daher beschloss ein Teil der Expert Group, zu versuchen, MVC unabhängig von Oracle weiterzuentwickeln. Glücklicherweise stimmte Oracle der Idee zu und übertrug Anfang 2017 den Specification-Lead-Posten. Seitdem wird MVC durch die Community weiterentwickelt und steht heute kurz vor dem finalen Release.
Das MVC-Entwurfsmuster
Zugegebenermaßen ist der Name MVC für die Spezifikation nicht ganz glücklich gewählt. Es wurde mehrfach argumentiert, dass auch JSF ein MVC-Framework sei, was sicherlich nicht falsch ist. MVC hat in seinen Ursprüngen nichts mit Webanwendungen zu tun, es ist entstanden in den späten siebziger Jahren, im Kontext von Benutzeroberflächen für Smalltalk. In Bezug auf Webanwendungen wird mit dem Begriff meist die Aufteilung der Verantwortlichkeiten bei aktionsbasierten Web-Frameworks beschrieben. Abbildung 1 zeigt das typische Verarbeitungsmodell eines solchen Frameworks.
Abb. 1: Das MVC-Entwurfsmuster
Der Lebenszyklus beginnt stets mit der Anfrage eines Clients. Anhand des angefragten URL wird der Request einem Controller zugeordnet. Dieser Schritt wird auch als Controller Matching bezeichnet. Der Controller bestimmt anhand der im Request enthaltenen Informationen, was genau zu tun ist. Das kann ein Zugriff auf die Datenbank oder Ähnliches sein. Eine der wichtigsten Aufgaben des Controllers ist es, das Model zu aktualisieren. Der Begriff Model ist an dieser Stelle ein eher abstraktes Konzept. Einfach gesprochen handelt es sich beim Model um eine Datenstruktur, die später für die Darstellung verwendet wird. Im nächsten Schritt wählt der Controller eine View, die für die Darstellung verwendet wird. Diese generiert die Antwort an den Client, meist in Form eines HTML-Dokuments. Dazu greift sie auf die Daten des Models zu, vom Controller zuvor für die View vorbereitet wurden.
Wie man sieht, ist das MVC-Entwurfsmuster in seiner Kernidee sehr einfach und deutlich besser zu durchschauen als beispielsweise der recht komplexe JSF-Lifecycle. Gerade diese Einfachheit macht das aktionsbasierte Modell interessant für viele Anwendungsgebiete.
Hello World
Nun schauen wir uns das API von MVC 1.0 anhand des klassischen Hello-World-Beispiels einmal genauer an. Listing 1 zeigt im ersten Schritt den entsprechenden Controller.
Listing 1
@Controller @Path("/hello") public class HelloController { @Inject private Models models; @GET public String greet() { models.put( "message", "Hello world!" ); return "helloworld.jsp"; } }
An dieser Stelle fällt auf, dass der Controller gewisse Ähnlichkeit zu einer JAX-RS-Ressource hat. Das liegt daran, dass ein MVC Controller eine spezielle JAX-RS-Ressource ist. Ist man also mit dem JAX-RS API vertraut, kann man sein Wissen auch für MVC verwenden, da sowohl das Path Matching als auch viele weitere Aspekte genau wie bei JAX-RS funktionieren. So trägt die Klasse eine @Path-Annotation, mit der dem Controller das URL-Prefix /hellozugeordnet wird. Außerdem befindet sich an der einzigen Methode der Klasse eine @GET-Annotation. Mit dieser Annotation definiert man, dass die Methode für die Behandlung von HTTP GET Requests zuständig ist.
Der erste wesentliche Unterschied zu einer normalen JAX-RS-Ressource ist die @Controller-Annotation. Mit ihr wird eine JAX-RS-Ressource zu einem MVC Controller. Die Annotation kann sowohl auf der Klasse als auch auf einzelne Methoden angewendet werden. Letzteres ist jedoch nur dann sinnvoll, wenn man hybride Controller erstellen möchte, bei denen sich einige Methoden wie MVC Controller und andere wie JAX-RS-Ressourcen verhalten sollen.
Die Controller-Methode liefert einen einfachen String als Rückgabewert. Genau hier zeigt sich einer der wesentlichen Unterschiede zwischen JAX-RS-Ressourcen und einem MVC Controller. Während bei einer JAX-RS-Ressource der Rückgabewert der Methode als Antwort für den Client interpretiert wird, gibt ein MVC Controller damit den Namen der zu verwendenden View an.
Die MVC-Implementierung erwartet die Views standardmäßig im Verzeichnis /WEB-INF/views/, was sich aber auf Wunsch auch ändern lässt. Die MVC-1.0-Spezifikation unterstützt von Haus aus JSPs und die aus JSF bekannten Facelets als View-Technologien. Dazu aber später mehr. Die in diesem Beispiel verwendete View ist als JSP-Datei realisiert und in Listing 2 zu sehen.
Listing 2
<!DOCTYPE html> <html> <head> <title>MVC 1.0 Beispiel</title> </head> <body> <h1>${message}</h1> </body> </html>
Auf den ersten Blick ist die View kaum als JSP-Datei zu erkennen. Nur der im Body verwendete EL-Ausdruck fällt ins Auge. JSP-Views können per Expression Language (EL) auf das Model zugreifen und erhalten somit Zugriff auf alle Daten, die für das Generieren des HTML-Dokuments benötigt werden.
Doch wie kommen die Daten ins Model? Wie zuvor beschrieben, ist es gemäß dem MVC-Entwurfsmuster Aufgabe des Controllers, das Model für die View vorzubereiten. Und genau das macht der Controller aus Listing 1 auch. Hier sei betont, dass MVC Controller nicht nur spezielle JAX-RS-Ressourcen sind, sondern auch CDI Beans. Controller können also alle Vorzüge des CDI-Komponentenmodells nutzen, vor allem die Dependency Injection, was unser Controller in diesem Fall macht. Er injiziert sich mittels @Injecteine Instanz der Klasse javax.mvc.Models. Hierbei handelt es sich um ein von MVC bereitgestelltes Bean, das dem Controller Zugriff auf das Model erlaubt. Die MVC-Klasse Modelsähnelt in gewisser Weise einer Map<String,Object>. Der Controller kann beliebige Java-Objekte unter einem frei wählbaren Namen im Model speichern. Die View kann diesen Namen nutzen, um an die Daten zu gelangen. In unserem Beispiel wird der Text Hello World!unter dem Namen messageim Model gespeichert, sodass die View über den EL-Ausdruck ${message}darauf zugreifen kann.
View Engines
Wie im vorigen Abschnitt erwähnt, unterstützt MVC 1.0 von Haus aus JavaServer Pages (JSP) und die aus JavaServer Faces (JSF) bekannten Facelets. Wer bereits mit JSF gearbeitet hat, wird Facelets sicherlich zu schätzen wissen. Besonders die Mechanismen für hierarchische Templates sind sehr flexibel und erlauben es, selbst komplexe Layouts mit wenig Aufwand umzusetzen. In der frühen Phase der Entwicklung von Java EE 8 war zwischenzeitlich die Idee aufgekommen, Facelets aus JSF herauszutrennen und als eigenständige Template-Engine für Java EE zu spezifizieren. Leider ist es bei der Idee geblieben, Facelets sind auch weiterhin stark an JSF gekoppelt. Das bedeutet leider auch, dass bei der Nutzung von Facelets als MVC-1.0-View-Technologie während der Generierung der View stets ein JSF Lifecycle gestartet wird. Aus Sicht der Performance nicht optimal, es lässt sich aber nicht vermeiden.
Bei JavaServer Pages denken viele Leser als Erstes an Legacy-Anwendungen aus früheren Zeiten. Das kommt auch daher, dass JSPs in der JSF-Welt bereits vor einiger Zeit von Facelets abgelöst wurden. Im Bereich der aktionsbasierten Web-Frameworks ist JSP allerdings alles andere als unüblich. Und das mit gutem Grund. Auch wenn JSPs etwas in die Jahre gekommen sind, handelt es sich dabei um eine stabile und wohlbekannte Technologie. Viele Entwickler sind vertraut mit JSP, und die Unterstützung in den IDEs ist ausgereift und stabil. In vielen Fällen ist also die Verwendung von JSP als MVC-1.0-View-Technologie auch heute noch sinnvoll.
Trotzdem darf man nicht vernachlässigen, dass es heutzutage viele alternative und modernere Ansätze für Template-Engines gibt, deren Fähigkeiten über die von JSP hinausgehen. Und genau hier kann MVC eine weitere große Stärke ausspielen. Mit dem View Engine SPI können Entwickler nach Belieben andere View-Technologien mit MVC nutzen. Dazu muss lediglich das Interface ViewEngineimplementiert werden, das vom genauen Mechanismus zur Erzeugung der HTML-Dokumente abstrahiert. Das View Engine SPI im Detail zu erläutern, würde den Rahmen dieses Artikels sprengen, doch glücklicherweise muss man in den seltensten Fällen selbst eine View Engine implementieren. Eclipse Krazo, die Referenzimplementierung von MVC 1.0, bringt beispielsweise Extensions für zwölf weitere View-Technologien bereits mit. Bei den unterstützten Engines finden sich Thymeleaf, Freemarker, Velocity, Handlebars, Jade, Mustache, StringTemplate und viele mehr. Besonders die Unterstützung von Thymeleaf ist von enormer Bedeutung, da es neben JSPs eine der wichtigsten View-Technologien im Spring-MVC-Umfeld ist.
Formulare
Bisher haben wir gelernt, wie ein Controller Daten für eine View zusammenstellen kann, die anschließend von der View in ein HTML-Dokument transformiert werden. Auch wenn sich damit bereits einige Anforderungen umsetzen lassen, kommt doch kaum eine Anwendung ohne eine Art von Benutzereingabe aus. Bei sehr interaktiven Anwendungen, bei denen viel JavaScript zum Einsatz kommt, werden Eingaben üblicherweise im JSON-Format per Ajax an den Server gesendet und von einer JAX-RS-Ressource verarbeitet. In diesen Fällen bietet Java EE alles, was der Entwickler für die Umsetzung benötigt. Aktionsbasierte Web-Frameworks setzen dagegen oft auf die klassischen HTML-Formulare, die im Vergleich zu anderen Ansätzen auch weiterhin viele Vorteile haben.
MVC 1.0 ergänzt die Fähigkeiten von JAX-RS bezüglich der Verarbeitung von Formulardaten in Aspekten, die für Web-Frameworks eine besondere Bedeutung haben. Dazu gehört vor allem das verbessertes Data Binding, das sowohl die Fehlerbehandlung als auch die Möglichkeit zur Internationalisierung deutlich erleichtert.
Listing 3
<form action="./form" method="POST"> Bitte geben Sie Ihren Namen ein: <input type="text" name="name"> Bitte geben Sie Ihr Alter ein: <input type="text" name="age"> <input type="submit" value="Absenden"/> </form>
Listing 3 zeigt ein einfaches HTML-Formular. Der Nutzer wird gebeten, seinen Namen und sein Alter in Formularfelder einzugeben. Es ist unerheblich, mit welcher View-Technologie das Formular erzeugt wurde. Der wichtigste Aspekt des Formulars sind die name-Attribute der Formularfelder, die genutzt werden, um die eingegebenen Werte an den Server zu übertragen.
Um ein solches Formular mit Hilfe von MVC 1.0 verarbeiten zu können, sollte zunächst eine Formularklasse erstellt werden, die die Werte der Formularfelder aufnimmt. Listing 4 zeigt, wie eine solche Klasse für das gezeigte Formular aussehen könnte.
Listing 4
public class HelloForm { @MvcBinding @FormParam("name") @Size(min = 2, message = "Geben Sie Ihren Namen ein") private String name; @MvcBinding @FormParam("age") @Min(value = 18, message = "Sie müssen 18 Jahre sein") private Integer age; /* Getter + Setter */ }
Man sieht, dass es sich bei der Formularklasse um ein einfaches Java Bean mit einer Property je Eingabefeld handelt. Die Felder der Klasse sind mit einer Reihe von Annotationen angereichert. Die @FormParam-Annotation stammt aus der JAX-RS-Spezifikation und wird verwendet, um einen konkreten Formularwert an die Property zu binden. Zu diesem Zweck gibt man den Wert des name-Attributs des jeweiligen Input-Elements an. Die @MvcBinding-Annotation aktiviert die speziellen MVC-Binding-Regeln. Was das genau bedeutet, werden wir im Folgenden betrachten.
Wie erwähnt, hat MVC 1.0 in vielen Bereichen auf bestehende Funktionalität der Java-EE-Plattform zurückgegriffen, so auch bei der Validierung von Eingabedaten. Daher können für Formularklassen Bean-Validation-Annotationen verwendet werden, um auf deklarative Art und Weise die Anforderungen an gültige Werte zu beschreiben. Die Formularklasse in Listing 3 verwendet @Size, um eine minimale Länge für den Namen zu definieren, und @Min, um zu prüfen, ob das Alter des Nutzers mindestens 18 Jahre beträgt.
Der kritische Leser wird bemerken, dass die Formularklasse eine recht große Menge an Annotationen enthält, die die Klasse aufblähen. Es befinden sich auf jedem Feld drei Annotationen. Hier darf man aber nicht vergessen, dass es sich lediglich um ein einfaches Java Bean mit einigen Feldern und den passenden Gettern und Settern handelt. Die Klasse ist also von Natur aus eher anämisch und enthält keinerlei spannende Logik. Daher ist die Anreicherung der Klasse um Metadaten in der Praxis kein wirkliches Problem für die Lesbarkeit des Quellcodes.
Listing 5
@Controller @Path("/form") public class FormController { @Inject private BindingResult bindingResult; @POST public String post( @BeanParam @Valid HelloForm form ) { if( bindingResult.isFailed() ) { models.put( "messages", bindingResult.getAllMessages() ); return "form.jsp"; } // Verarbeitung des Forms String name = form.getName(); Integer age = form.getAge(); } }
Hat man die entsprechende Formularklasse geschrieben, kann der Controller sie nutzen, um auf die Daten des Formulars zuzugreifen. Listing 5 zeigt einen zu dem HTML-Formular aus Listing 4 passenden Controller. Die Grundstruktur des Controllers ist identisch zu den vorherigen Beispielen. Neu ist, dass sich der Controller eine Instanz der Klasse BindingResultinjiziert. Wie der Name der Klasse vermuten lässt, erlaubt sie den Zugriff auf die Ergebnisse der Data-Binding-Phase.
Die Methode des Controllers nutzt statt einer @GET– eine @POST-Annotation und ist somit zuständig für HTTP POST Requests. Außerdem erhält sie eine Instanz der Formularklasse als Parameter. Mittels der @BeanParam-Annotation teilt der Controller der JAX-RS-Implementierung mit, dass die Formularklasse nach Data-Binding-Annotationen, beispielsweise @FormParam, durchsucht werden soll. Außerdem legt er mit der @Valid-Annotation fest, dass alle durch Bean-Validation-Annotationen definierten Bedingungen auf valide Werte geprüft werden sollen.
Hier ist es wichtig zu verstehen, wie eine normale JAX-RS-Ressource mit Fehlern beim Data Binding umgehen würde. Sendet der Client ungültige Daten, die beispielsweise nicht in den entsprechenden Java-Datentyp konvertiert werden können oder gemäß der Bean Validation Constraints nicht als valide gelten, würde die JAX-RS-Implementierung die Methode der Ressource gar nicht erst aufrufen und stattdessen dem Client mit einem HTTP-Statuscode 400 („Bad Request“) antworten. Dieses Verhalten ist zwar aus semantischer Sicht vollkommen richtig, allerdings nicht für die Formularverarbeitung in einer Webanwendung geeignet. Hat ein Benutzer ungültige Daten in ein Formular eingetragen, erwartet er eine aussagekräftige Fehlermeldung und die Möglichkeit, seine Eingaben zu korrigieren. Das heißt, in einer Webanwendung müsste ein Controller wieder die Erstellung eines HTML-Dokuments einleiten.
Genau an diesem Punkt setzt das erweiterte Data Binding von MVC an. Wird ein Data Binding zusätzlich mit der @MvcBinding-Annotation versehen, ändert sich das Verhalten im Fehlerfall. Statt die Verarbeitung des Requests komplett abzubrechen, wird der Controller trotzdem ausgeführt und erhält über die Klasse BindingResult die Möglichkeit, auf Details zum Fehler zuzugreifen. Genau das macht auch der Controller aus Listing 5. Im ersten Schritt überprüft er mittels der Methode isFailed(), ob Fehler beim Binden der Formulardaten aufgetreten sind. Ist das der Fall, greift er mit getAllMessages() auf die entsprechenden Fehlermeldungen zu und schreibt sie ins Model, damit die View sie im HTML-Dokument darstellen kann. Die Klasse BindingResult bietet noch einige andere Methoden an, mit denen auf mehr Details zu einzelnen Fehlern zugegriffen werden kann. Das kann dann nützlich sein, wenn man Zugriff auf die ConstraintViolationbenötigt, die bei einer fehlgeschlagenen Validierung durch Bean Validation alle nötigten Informationen enthält.
War das Binden der Daten erfolgreich, kann der Controller über die Getter der Formularklasse auf die übermittelten Daten zugreifen. JAX-RS unterstützt bereits die Konvertierung der gängigsten Datentypen, sodass die Formulardaten meist direkt weiterverarbeitet werden können.
Was gibt es noch?
Bezüglich des Funktionsumfangs versucht MVC 1.0 einen Mittelweg zu finden. Einerseits versteht sich die Spezifikation als eher dünne Schicht über den anderen Java-EE-Technologien wie JAX-RS, CDI und Bean Validation, anderseits sollten natürlich die wichtigsten Bedürfnisse bei der Entwicklung von Webanwendungen abgedeckt sein, damit sich die Arbeit mit MVC möglichst einfach gestaltet. Daher bietet MVC neben den bisher beschriebenen Mechanismen einige weitere Funktionen.
Da das Thema Sicherheit bei Webanwendungen von zentraler Bedeutung ist, bringt MVC bereits zwei wichtige Features mit: zum einen einen Mechanismus zur Abwehr von Cross-Site Request Forgery (CSRF), sodass der Entwickler hier bereits das nötige Werkzeug zur Härtung der Anwendung in die Hand bekommt. Außerdem erlaubt MVC die einfache Kodierung von Daten beim Erzeugen der HTML Views zur Verhinderung von Cross-Site Scripting. Das ist besonders für JSP Views wichtig, da JSPs im Gegensatz zu anderen Technologien das Escaping nicht selbst durchführt.
Darüber hinaus bietet MVC spezielle Unterstützung für das populäre Post-Redirect-Get-(PRG-)Pattern, das besonders bei aktionsbasierten Web-Frameworks sehr wichtig für eine gute User Experience ist. Zum einen definiert MVC einen einfachen Weg, aus einem Controller heraus Redirects zu erzeugen, zum anderen bietet der CDI Scope @RedirectScopeddie Möglichkeit, Daten über einen Redirect hinweg zu erhalten. Außerdem erzeugt MVC 1.0 während der Verarbeitung eines Requests eine Reihe von CDI Events, die Entwickler für die Implementierung verschiedener Cross-Cutting Concerns nutzen können. Zu guter Letzt adressiert MVC auch das Thema Internationalisierung von Webanwendungen. So ist es sehr einfach möglich, Aspekte wie das Data Binding von numerischen Datentypen und die Erzeugung korrekt lokalisierter Fehlermeldungen im Fall von Validierungsfehlern an die Sprache des jeweiligen Nutzers anzupassen.
Fazit
Nachdem die MVC-1.0-Spezifikation bedingt durch ihre turbulente Geschichte länger gebraucht hat als zunächst geplant, steht sie endlich kurz vor ihrer Fertigstellung. Inzwischen sind alle durch den JCP vorgeschriebenen Meilensteine erreicht, sodass in den nächsten Wochen mit der Veröffentlichung der finalen Version gerechnet werden kann. Aber auch die nächsten Schritte sind bereits geplant. Nachdem MVC 1.0 es leider nicht in Java EE 8 geschafft hat, ist nun das Ziel, offizieller Bestandteil von Jakarta EE 9 zu werden. Die Prognose sieht positiv aus. So hat die Jakarta-EE-Community bereits positiv auf den Plan reagiert, und auch die MVC-1.0-Referenzimplementierung Eclipse Krazo ist bereits Teil des Eclipse-EE4J-Projekts. Man kann also gespannt sein, wie es mit MVC weitergeht.
Java-Dossier für Software-Architekten 2019
Mit diesem Dossier sind Sie auf alle Neuerungen in der Java-Community vorbereitet. Die Artikel liefern Ihnen Wissenswertes zu Java Microservices, Req4Arcs, Geschichten des DevOps, Angular-Abenteuer und die neuen Valuetypen in Java 12.